#
# Copyright (C) 2008-2009 Dag Brattli, ObexCode
#
# Portions copyright 2007 Google Inc. (GData Python Client Library)
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import logging
from datetime import datetime

try:
    import simplejson
except ImportError:
    try:
        from django.utils import simplesjon # App Engine friendly
    except ImportError:
        pass

import util

log = logging.getLogger('elements')

# Import ElementTree API. You can override it by setting the ET class member
try:
    from xml.etree import cElementTree as ET
except ImportError:
    try:
        import cElementTree as ET
    except ImportError:
        try:
            from xml.etree import ElementTree as ET
        except ImportError:
            from elementtree import ElementTree as ET

class Element(object):
    """Element is the base class to be used for your XML objects. You will
    need to subclass Element for all XML elements you want to parse."""
    
    _children = {}
    _attributes = {}
    _namespaces = None
    _namespace = None
    
    ET = ET # So you can override and use a different ElementTree API 

    def __init__(self, text=None):
        self.text = text

        # You probably don't want to put any code here. It's because the
        # Elements model declaration does not usually call the super class
        # constructor, so this method is not usually called.
        
    def set_default_namespace(self, namespace):
        """Set default namespace.
        
        The default namespace will then be used in case the case Element
        supports multiple namespaces and does not have a specified namespace
        """
                
        # If we don't have a namespace we use the supplied default namespace
        if not self._namespace:
            if self._namespaces:
                # Check if default namespace is valid for us
                if namespace in self._namespaces:
                    self._namespace = namespace
                else:
                    raise ValueError("namespace %s not valid" % namespace)
            else: 
                self._namespace = namespace or self._namespace
    
    @classmethod
    def copy_children(cls, default_ns=None):
        """Copy children from subclassed Element. 
        
        It's important that we make all the children tag names qualified with
        namespace, in case the children uses the default namespace of the 
        Element. In some cases the Element does not have a default namespace 
        if it accepts multiple namespace versions, and we thus have to 
        supply the current default namespace being parsed.
        """
        children = {}
        for tag, value in cls._children.items():
            if tag[0] != '{':
                if cls._namespace:
                    tag = "{%s}%s" % (cls._namespace, tag)
                elif default_ns:
                    tag = "{%s}%s" % (default_ns, tag)
            children[tag] = value
        return children
        
    @classmethod
    def copy_attributes(cls):
        return cls._attributes.copy()
    
    def __setattr__(self, name, value):
        """This is a bit of magic. We override setattr in order to record the 
        ordering of the member variables in the __init__ method. This way we 
        can force the same ordering when converting back to ElementTree/XML 
        again"""
        
        if not hasattr(self, '_ordering'):
            object.__setattr__(self, '_ordering', [])
        if name not in self._ordering:
            self._ordering.append(name)
        
        object.__setattr__(self, name, value)
        
    def _convert_element_tree_to_member(self, child_tree):
        if child_tree.tag[0] == '{':
            ns, tag = child_tree.tag[1:].split("}", 1)
        else:
            tag = child_tree.tag
            ns = None

        # Find the element's tag in this class's list of children
        if self.__class__._children.has_key(tag) and (ns == self._namespace):
            child = self.__class__._children[tag]
        elif self.__class__._children.has_key(child_tree.tag):
            child = self.__class__._children[child_tree.tag]
        else:
            log.warning('convert_element_tree_to_member(): '
                                        'Unknown member %s' % child_tree.tag)
            return
        
        member_name = child[0]
        
        # If no class, then we assume the same class as ourselves. This is to
        # enable Elements classes to be self referencing.
        try:
            member_class = child[1]
        except IndexError:
            member_class = self.__class__
        
        # If the class member is supposed to contain a list, make sure the
        # matching member is set to a list, then append the new member
        # instance to the list.
        
        # Process types first. If an object is an instance of <type 'type'>,
        # then it is a type. Otherwise, it is a non-type.
        if isinstance(member_class, type):
            if issubclass(member_class, basestring):
                if issubclass(member_class, unicode):
                    setattr(self, member_name, child_tree.text)
                else:
                    setattr(self, member_name, str(child_tree.text))

            elif member_class is bool:
                setattr(self, member_name, True)

            elif member_class is int:
                if child_tree.text.strip():
                    setattr(self, member_name, int(child_tree.text))
                else:
                    setattr(self, member_name, None)
                
            elif member_class is float:
                setattr(self, member_name, float(child_tree.text))
            
            elif member_class is datetime:
                import dateutil.parser
                if child_tree.text.strip():
                    setattr(self, member_name, 
                                    dateutil.parser.parse(child_tree.text))
                else:
                    setattr(self, member_name, None)
            else:
                new_member = member_class()
                # set default namespace if namespace is missing
                new_member.set_default_namespace(ns or self._namespace)
                
                new_member.from_element_tree(child_tree)
                setattr(self, member_name, new_member)
        else:
            # ElementTree is a classic class <type 'classobj'>
            if type(member_class) is type(ET.ElementTree):
                setattr(self, member_name, 
                                ET.ElementTree(child_tree.getchildren()[0]))
            
            elif isinstance(member_class, list):
                # Make sure member is a list
                if getattr(self, member_name) is None:
                    setattr(self, member_name, [])

                # If member class is missing, the we assume the same class as 
                # we are. This is a feature to enable self referencing Elements
                try:
                    new_class = member_class[0]
                except IndexError:
                    new_class = self.__class__

                if issubclass(new_class, Element):
                    new_member = new_class()
                    # set default namespace if namespace is missing
                    if not new_member._namespace and not new_member._namespaces:
                        new_member._namespace = self._namespace
                    new_member.from_element_tree(tree=child_tree)   
                    getattr(self, member_name).append(new_member)
                elif issubclass(new_class, unicode):
                    getattr(self, member_name).append(child_tree.text)
                elif issubclass(new_class, int):
                    getattr(self, member_name).append(int(child_tree.text))
                else:
                    log.warning('convert_element_attribute_to_member(): '
                            'Unknown member class %s' % repr(member_class[0]))
            
            elif isinstance(member_class, dict):
                key = member_class.keys()[0]
                new_member = member_class[key]()
            
                new_member.from_element_tree(tree=child_tree)
            
                if key[0] == '{' or not new_member._namespace:
                    index = child_tree.findtext('%s' % key)
                else:
                    index = child_tree.findtext('{%s}%s' % \
                        (new_member._namespace, key))
                
                getattr(self, member_name)[index] = new_member

    def _convert_element_attribute_to_member(self, attribute, value):
        # Find the attribute in this class's list of attributes. 
        if not self.__class__._attributes.has_key(attribute):
            log.warning('convert_element_attribute_to_member(): '
                                        'Unknown attribute %s' % attribute)
            return
            
        # Find the member of this class which corresponds to the XML attribute
        # (lookup in current_class._attributes) and set this member to the
        # desired value (using self.__dict__).
        if value:
            # Check for new style attribute encoding ('attribute_name', type)
            if type(self.__class__._attributes[attribute]) == tuple:
                attrib_name = self.__class__._attributes[attribute][0]
                attrib_class = self.__class__._attributes[attribute][1]
            
                if issubclass(attrib_class, basestring):
                    setattr(self, attrib_name, unicode(value))
                elif issubclass(attrib_class, bool):
                    setattr(self, attrib_name, 
                            bool(value == 'True' or value == '1'))
                elif issubclass(attrib_class, int):
                    setattr(self, attrib_name, int(value))
                elif issubclass(attrib_class, float):
                    setattr(self, attrib_name, float(value))
                else:
                    setattr(self, attrib_name, unicode(value))
            else:
                # Old style attribute encoding 'attribute-name'
                setattr(self, self.__class__._attributes[attribute], 
                    unicode(value))


    # Three methods to create an object from an ET
    def _harvest_element_tree(self, tree):
        # Fill in the instance members from the contents of the XML tree.

        for child in tree:
            self._convert_element_tree_to_member(child)
        for attribute, value in tree.attrib.iteritems():
            self._convert_element_attribute_to_member(attribute, value)

        self.text = tree.text

    # Helper function to add string children to an ET
    def _add_string_to_element_tree(self, tag, member):
        new_child = self.ET.Element(tag)
        new_child.text = member
        return new_child

    # Three methods to create an ET from an object
    def _add_members_to_element_tree(self, tree):
        # Convert the members of this class which are XML child nodes. 
        # This uses the class's _children dictionary to find the members which
        # should become XML child nodes.
        
        children = self.__class__.copy_children()
        items = children.items()
        # Sort children according to ordering listed in __init__() method
        items.sort(cmp=lambda x,y: cmp(self._ordering.index(x[1][0]), 
                      self._ordering.index(y[1][0])))

        for tag, value in items:
            if tag[0] != '{' and self._namespace:
                tag = "{%s}%s" % (self._namespace, tag)
                ns = self._namespace
            else:
                ns = tag[1:].split("}", 1)[0]

            member_name = value[0]
            member = getattr(self, member_name)

            # Check if children ns matches member namespace
            if getattr(member, "_namespace", None) and ns != member._namespace:
                # Namespace does not match, so we skip this child. It's 
                # probably declared multiples times to support different
                # namespace versions for the same element
                continue

            # Skip members with None as value
            if member is None:
                continue
                            
            elif isinstance(member, basestring):
                new_child = self._add_string_to_element_tree(tag, member)
                tree.append(new_child)
            elif isinstance(member, bool):
                # Only include bool elements that are True
                if member:
                    new_child = self.ET.Element(tag)
                    tree.append(new_child)
            elif isinstance(member, int) or isinstance(member, float):
                new_child = self._add_string_to_element_tree(tag, str(member))
                tree.append(new_child)              
            elif isinstance(member, list):
                for instance in member:
                    if isinstance(instance, basestring): # List of strings
                        new_child = self._add_string_to_element_tree(tag, 
                            instance)
                        tree.append(new_child)
                    elif isinstance(instance, int): # List of integers
                        new_child = self._add_string_to_element_tree(tag, 
                            str(instance))
                        tree.append(new_child)
                    else: # List of Elements subclass
                        instance._become_child_element(tree, self._namespace)
                        
            elif isinstance(member, dict):
                for instance in member.values():
                    instance._become_child_element(tree, self._namespace)
            elif isinstance(member, ET.ElementTree):
                new_child = self.ET.Element(tag)
                new_child.append(member.getroot())
                tree.append(new_child)
            elif isinstance(member, datetime):
                new_child = self._add_string_to_element_tree(tag, 
                    member.strftime(value[2]))
                tree.append(new_child)
            else:
                member._become_child_element(tree, self._namespace)
                
        # Convert the members of this class which are XML attributes.
        for xml_attribute, attribute in self.__class__._attributes.iteritems():
            if type(attribute) == tuple:
                attrib_name = attribute[0]
                attrib_class = attribute[1]

                member = getattr(self, attrib_name)
                if member is not None:
                    tree.attrib[xml_attribute] = unicode(member)
            else:
                member = getattr(self, attribute)
                if member is not None:
                    tree.attrib[xml_attribute] = unicode(member)
    
        if hasattr(self, 'text'):
            tree.text = self.text
    
    def _become_child_element(self, tree, default_ns=None):
        """
        Note: Only for use with classes that have a _tag and _namespace class
        member. It is in Elements so that it can be inherited but it should
        not be called on instances of Elements.
        """
        if not self._namespace:
            # Element does not have a specified namespace, use parent 
            # namespace instead
            self._namespace = default_ns

        tag = "{%s}%s" % (self._namespace, self.__class__._tag)

        new_child = self.ET.Element(tag)
        tree.append(new_child)
        self._add_members_to_element_tree(new_child)

    def to_element_tree(self):
        """Convert this Elements object to an ElementTree"""
        if self._namespace:
            tag = "{%s}%s" % (self._namespace, self.__class__._tag)
            nsmap = { None : self._namespace }
        else:
            tag = self.__class__._tag
            nsmap = None
        
        new_tree = self.ET.Element(tag) #, nsmap=nsmap)
        self._add_members_to_element_tree(new_tree)
        return new_tree

    def from_element_tree(self, tree):
        """Parse ElementTree into this Element object"""
        
        if self._namespace:
            match = "{%s}%s" % (self._namespace, self._tag)
        elif self.__class__._namespaces:
            match = ""
            for namespace in self.__class__._namespaces:
                if tree.tag == "{%s}%s" % (namespace, self._tag):
                    match = tree.tag
                    
                    # Set default namespace
                    self._namespace = namespace
                    break
        else:
            match = self._tag
        
        if tree.tag == match:
            self._harvest_element_tree(tree)
            ret = True
        else:
            log.warning('from_element_tree(): '
                                        'Unknown element tag %s' % tree.tag)
            ret = False
        return ret

    def to_string(self, encoding=None, indent=True, use_default_ns=True):
        """Convert this Elements object to XML string"""
        
        etree = self.to_element_tree()
        if indent:
            util.indent(etree)
        if use_default_ns:
            util.use_default_ns(etree)
        return self.ET.tostring(element=etree, encoding=encoding)

    def from_string(self, xml_string, encoding=None):
        """Parse XML string into this Elements object. Assumes the string
        is unicode or ascii. If not, specify the encoding"""
        
        # ET.fromstring wants to receive utf-8 encoded byte strings
        if isinstance(xml_string, unicode):
            xml_string = xml_string.encode('utf-8')
        elif encoding:
            xml_string = xml_string.decode(encoding).encode('utf-8')
        
        tree = self.ET.fromstring(xml_string)
        return self.from_element_tree(tree)
        
    def from_file(self, filename, encoding='utf-8'):
        """Parse XML file into this Elements object. Assumes the content
        is utf-8. If not, then specify the encoding"""
        
        f = open(filename)
        ret = self.from_string(f.read(), encoding)
        f.close()
        return ret
        
    def from_form(self, **params):
        """Initialize an element from form data"""
        raise NotImplemented
        
    def __str__(self):
        """Returns the text contents of the element"""
        return self.text or ""

# Backward compability
Elements = Element

class JsonElement(Element):
    """An Element that can convert to/from dictionaries
    
    JsonElement can be used for representing Elements as JSON instead of XML
    """
    
    _skip_empty = True    # Skip empty elements

    def _add_members_to_dict(self, result, root, default_ns=None, compact=False):
        children = self.__class__.copy_children()
        
        # Make sure the dict has a default namespace
        if not default_ns:
            default_ns = self.__class__._namespace
            result['xmlns'] = default_ns
        
        # Process all child elements
        for tag, value in children.items():
            if tag[0] == '{':
                ns, key = tag[1:].split("}", 1)
            else:
                ns, key = self._namespace, tag

            member_name = value[0]
            member = getattr(self, member_name)
                        
            # Check if children ns matches member namespace
            if getattr(member, "_namespace", None) and ns != member._namespace:
                # Namespace does not match, so we skip this children. It's 
                # probably a child that is declared multiple times to support
                # different namespace versions for the same element.
##                print member._namespace, ns, self._namespace
                continue

            # Now the difficult part. Handle namespaces aliases
            if ns != default_ns:
                if ns and ns != default_ns:
                    if self.__class__._namespace_aliases and (
                                    ns in self.__class__._namespace_aliases):
                        alias = self.__class__._namespace_aliases[ns]
                        root['xmlns$%s' % alias] = ns
                    else:
                        alias = ns
                    key = "%s$%s" % (alias, key)
                else:
                    key = key
            else:
                key = key

            if isinstance(member, list):
                new_list = []
                for instance in member:
                    if isinstance(instance, Element):
                        new_dict = {}
                        new = instance._add_members_to_dict(new_dict, root, 
                                                            default_ns, 
                                                            compact=compact)
                        new_list.append(new)
                    else:
                        new_list.append(instance)
                if new_list or not self._skip_empty:
                    result[key] = new_list
                    
            elif isinstance(member, Element):
                new_dict = {}
                new = member._add_members_to_dict(new_dict, root, default_ns,
                                                  compact=compact)
                result[key] = new
            elif isinstance(member, int) or isinstance(member, float):
                result[key] = member
            elif member is None:
                if not self._skip_empty:
                    result[key] = member
            else:
                if isinstance(member, datetime):
                    member = member.strftime(value[2])
                if compact:
                    result[key] = unicode(member)
                else:
                    result[key] = { "$t" : unicode(member) }
        
        # Process all attributes
        for xml_attribute, attribute in self.__class__._attributes.iteritems():
            if type(attribute) == tuple:
                attrib_name = attribute[0]
                attrib_class = attribute[1]

                member = getattr(self, attrib_name)
                if member is not None:
                    result[xml_attribute] = unicode(member)
            else:
                member = getattr(self, attribute)
                if member is not None:
                    result[xml_attribute] = unicode(member)

        # Process any text nodes
        if getattr(self, 'text', None) and self.text.strip():
            if result or not compact:
                # Add a text key to the resulting dict
                result['$t'] = self.text
            else:
                # Only text, so just skip the dict and return the text instead
                result = self.text

        return result
        
    def to_dict(self, compact=False):
        """Convert element to a dict, usable for JSON etc"""
        
        new_dict = {}
        return { self.__class__._tag : self._add_members_to_dict(new_dict,
                                root=new_dict, default_ns=None, 
                                compact=compact)}
    __json__ = to_dict

    def _harvest_dict(self, tree, dictionary, default_ns):
        def mycmp(a, b):
            if a[0].startswith("xmlns"):
                return 1
            elif b[0].startswith("xmlns"):
                return -1
            return cmp(a, b)

        # Do we need to support change of default namespace?
        default_ns = dictionary.pop('xmlns', default_ns)

        items = dictionary.items()
        items.sort(cmp=mycmp, reverse=True) # Make sure we process xmlns first

        children = self.__class__.copy_children(default_ns)
        for key, value in items:
            if '$' in key:
                ns = None
                alias, tag = key.split('$')
                if alias == "xmlns":
                    self._namespace_aliases[value] = tag
                    continue
                    
                for _ns, _alias in self._namespace_aliases.items():
                    if alias == _alias:
                        ns = _ns
                        break
            else:
                tag = key
                ns = default_ns
                
            # Check for text nodes first
            if key == '$t':
                tree.text = value
                continue
                        
            # Need to work with qualified names
            qname= "{%s}%s" % (ns, tag)
            if qname in children.keys():
                if isinstance(value, dict):
                    # Create Element from dict
                    new_child = self.ET.Element("{%s}%s" % (ns, tag))
                    tree.append(new_child)
                    member_class = children[qname][1]
                    if member_class is datetime:
                        new_child.text = value["$t"]
                    elif issubclass(member_class, Element):
                        elem = member_class()
                        elem._namespace_aliases = self._namespace_aliases.copy()
                        elem._harvest_dict(new_child, value, ns)
                    else:
                        self._harvest_dict(new_child, value, ns)
                elif isinstance(value, list):
                    for instance in value:
                        new_child = self.ET.Element("{%s}%s" % (ns, tag))
                        tree.append(new_child)
                        elem = children[qname][1][0]()
                        elem._namespace_aliases = self._namespace_aliases.copy()
                        elem._harvest_dict(new_child, instance, ns)
                else:
                    new_child = self.ET.Element("{%s}%s" % (ns, tag))
                    new_child.text = value
                    tree.append(new_child)
            
            # Attributes does not have namespaces
            elif tag in self._attributes.keys():
                # attribute
                tree.attrib[tag] = value
            else:
                log.warning("_harvest_dict(), " 
                                     "Unknown key: %s, namespace=%s", key, ns)
        
    def from_dict(self, value):
        """Convert from dict to element"""
        
        value = value[self._tag]
        default_ns = value.pop('xmlns', None)
        tree = self.ET.Element("{%s}%s" % (default_ns, self._tag))
        
        self._namespace_aliases = {}
        
        self._harvest_dict(tree, value, default_ns)
        self.from_element_tree(tree)
        
    def to_json(self):
        """Convert Element to JSON"""
        
        value = self.to_dict()
        return simplejson.dumps(value)

    def from_json(self, json):
        """Convert JSON to Element"""
        
        value = simplejson.loads(json)
        return self.from_dict(value)

__all__ = ['Element', 'Elements', 'JsonElement']
